AWS Secrets Managerのマルチユーザーローテーションを試してみた

AWS Secrets Managerのマルチユーザーローテーションを試してみた

AWS Secrets Managerのマルチユーザーローテーションを試してみました。アプリケーション側のキャッシュの影響が少なくなるので便利。
Clock Icon2022.03.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWS Secrets Managerのマルチユーザーローテーションがイマイチ分からない

こんにちは、のんピ(@non____97)です。

皆さんはAWS Secrets Managerのマルチユーザーローテーションを試したいと思ったことはありますか? 私はあります。

どうやらシングルユーザーローテーションと比べて、シークレットをアプリケーション側でキャッシュしていても、影響が少なくなる方式のようです。

しかし、圧倒的にやってみたブログが少ないので、動作確認してみました。

いきなりまとめ

  • マルチユーザーローテーションを行うと、DBに同じ権限を持ったユーザーを自動で作成してくれる
  • オリジナルのユーザーと自動で作成されたユーザーの2つのユーザーのパスワードを交互に更新して、ラベルを張り替える動きをする
  • マルチユーザーローテーションをする場合、別途シークレットのローテーションを管理するためのシークレットが必要になる
  • アプリケーション側でシークレット情報をキャッシュする場合の影響を少なくできる

マルチユーザーローテーションとは

まず、マルチユーザーローテーションとは何かを確認します。

AWS公式ドキュメントでは以下のように説明されています。(文中の「交代ユーザー」は「マルチユーザー」に読み替えてもらえたらと思います。)

交代ユーザー戦略は、1 つのシークレット内で 2 人のユーザーの認証情報を更新します。最初のユーザーを作成した後に、そのローテーションクローンを作成して 2 番目のユーザーを作成します。

シークレットの後続の各バージョンでは、ユーザーが交代で更新されます。例えば、最初のバージョンが user1/password1 持つ場合、2 番目のバージョンではそれが user2/password2 となります。3 番目のバージョンでは user1/password3 となり、4 番目は user2/password4 となります。任意の時点で、2 つの有効な認証情報セット (現在の認証情報と以前の認証情報) が存在することになります。

ローテーションによって新しいバージョンが作成されている間、アプリケーションは引き続き既存バージョンを使用します。新しいバージョンの準備ができたら、ローテーションによってステージングラベルが切り替えられ、アプリケーションが新しいバージョンを使用できるようになります。

それぞれのシークレットが、管理者またはスーパーユーザー用の認証情報を持っており、これにより、2 番目のユーザーを作成したり、両方のユーザーの認証情報を更新したりできます。

ローテーション戦略 - AWS Secrets Manager

はい、私は全く理解できませんでした。

最初の「最初のユーザーを作成した後に、そのローテーションクローンを作成して 2 番目のユーザーを作成します。」から難易度が高いです。

それでは、「習うより慣れろ」ということで実際にマルチユーザーローテーションを試してみます。

マルチユーザーローテーションを試してみた

検証環境の用意

まず、検証環境の用意をします。

以下ブログでAWS CDKを使ってAmazon Auroraのパスワードをシングルユーザーローテーションをするように定義しました。

検証環境を用意するにあたって、こちらで使用したコードを流用します。

準備ができたら、npx cdk deploy --allで各種リソースをデプロイします。

ローテーション制御用DBユーザーの作成

続いて、ローテーション制御用DBユーザーを作成します。

マルチユーザーローテーションでは、以下AWS公式ドキュメントで紹介されている通り、ローテーションさせたいシークレットとは別に、ローテーションを制御するためのシークレットが必要です。

この戦略を使用するには、もう 1 つのユーザーの作成と、両方のユーザーに対するパスワードの変更が許可された、管理者またはスーパーユーザーの認証情報を持つ別のシークレットが必要です。最初のローテーションでは、このユーザーをクローンして交代ユーザーを作成することで、両方のユーザーが同じ権限を持つようにします。

ローテーション戦略 - AWS Secrets Manager

そこで、DB(Amazon Aurora with PostgreSQL Compatibility)に接続して、DBユーザーを作成します。

まず、マネージメントコンソールから現在の認証情報を確認します。現在の認証情報は以下の通りでした。

{
  "dbClusterIdentifier": "prd-db-cluster",
  "password": "+2gf?F0bk3estdeXri#cq6$Ux{8}~crC",
  "dbname": "testDB",
  "engine": "postgres",
  "port": 5432,
  "host": "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com",
  "username": "postgresAdmin"
}

こちらの認証情報を使用してDBに接続します。

$ get_secrets_value=$(aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --region us-east-1 \
     | jq -r .SecretString)

$ export PGHOST=$(echo "${get_secrets_value}" | jq -r .host)
$ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port)
$ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname)
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

$ psql
psql (13.6, server 13.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

testDB=>
testDB=>
testDB=> show timezone;
  TimeZone
------------
 Asia/Tokyo
(1 row)

testDB=>
testDB=> select current_timestamp;
      current_timestamp
------------------------------
 2022-03-14 09:59:49.25155+09
(1 row)

testDB=>

接続できたので、DBにパスワードのローテーションを制御するユーザーを作成します。パスワードのローテーションができるように、ローテーション制御用ユーザーはユーザーの作成やパスワードの変更ができるような権限を持つ必要があります。今回はCREATEROLEを付与しました。

testDB=> \du
                                                               List of roles
    Role name    |                         Attributes                         |                          Member of
-----------------+------------------------------------------------------------+-------------------------------------------------------------
 postgresAdmin   | Create role, Create DB                                    +| {rds_superuser}
                 | Password valid until infinity                              |
 rds_ad          | Cannot login                                               | {}
 rds_iam         | Cannot login                                               | {}
 rds_password    | Cannot login                                               | {}
 rds_replication | Cannot login                                               | {}
 rds_superuser   | Cannot login                                               | {pg_monitor,pg_signal_backend,rds_replication,rds_password}
 rdsadmin        | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
                 | Password valid until infinity                              |

testDB=>
testDB=> CREATE ROLE "rotationPasswordUser" WITH CREATEROLE LOGIN PASSWORD 'rotationPasswordUser_password';
CREATE ROLE
testDB=>
testDB=>
testDB=> \du
                                                                  List of roles
      Role name       |                         Attributes                         |                          Member of
----------------------+------------------------------------------------------------+-------------------------------------------------------------
 postgresAdmin        | Create role, Create DB                                    +| {rds_superuser}
                      | Password valid until infinity                              |
 rds_ad               | Cannot login                                               | {}
 rds_iam              | Cannot login                                               | {}
 rds_password         | Cannot login                                               | {}
 rds_replication      | Cannot login                                               | {}
 rds_superuser        | Cannot login                                               | {pg_monitor,pg_signal_backend,rds_replication,rds_password}
 rdsadmin             | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
                      | Password valid until infinity                              |
 rotationPasswordUser | Create role                                                | {}

testDB=>

ローテーション制御用DBユーザーのシークレット作成

ローテーション制御用DBユーザーのシークレットを作成します。

こちらのシークレットもAWS CDKで定義します。ローテーション制御用DBユーザーのシークレットはシングルユーザーローテーションでローテーションするようにしました。

実際のコードは以下の通りです。

// User for password rotation
const dbRotationPasswordUserSecret = new secretsmanager.Secret(
  this,
  "DbRotationPasswordUserSecret",
  {
    secretName: "prd-db-cluster/rotationPasswordUserLoginInfo",
    secretStringBeta1:
      secretsmanager.SecretStringValueBeta1.fromUnsafePlaintext(
        '{"username": "rotationPasswordUser","password": "rotationPasswordUser_password"}'
      ),
  }
);
// Rotation of password rotation for user's secret for password rotation
new secretsmanager.SecretRotation(
  this,
  "DbRotationPasswordUserSecretRotationSingleUser",
  {
    application:
      secretsmanager.SecretRotationApplication
        .POSTGRES_ROTATION_SINGLE_USER,
    secret: dbRotationPasswordUserSecret,
    target: dbCluster,
    vpc: props.vpc,
    automaticallyAfter: Duration.days(3),
    excludeCharacters: EXCLUDE_CHARACTERS,
    securityGroup: rotateSecretsLambdaFunctionSg,
    vpcSubnets: props.vpc.selectSubnets({
      subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
    }),
  }
);

// DB Client IAM role
const dbClientIamRole = new iam.Role(this, "DbClientIamRole", {
  assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName(
      "AmazonSSMManagedInstanceCore"
    ),
    new iam.ManagedPolicy(this, "GetSecretValueIamPolicy", {
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          resources: [dbAdminSecret.secretArn,dbRotationPasswordUserSecret.secretArn],
          actions: ["secretsmanager:GetSecretValue","secretsmanager:ListSecretVersionIds"],
        }),
      ],
    }),
  ],
});

npx cdk deploy DatabaseStackで変更を更新します。

デプロイ後にローテーション制御用DBユーザーのシークレットを確認すると、以下のようになっていました。

{
  "username": "rotationPasswordUser",
  "password": "rotationPasswordUser_password",
}

AWS公式ドキュメントに記載されている以下のようなシークレットの構造ではないので、これではローテーションできません。

{
  "engine": "postgres",
  "host": "<required: instance host name/resolvable DNS name>",
  "username": "<required: username>",
  "password": "<required: password>",
  "dbname": "<optional: database name. If not specified, defaults to 'postgres'>",
  "port": "<optional: TCP port number. If not specified, defaults to 5432>"
}

そのため、手動でenginehostなどを追加していきます。

ローテーション用ユーザーのシークレット編集

追加後、手動でシークレットをローテーションさせると、以下のようにパスワードがデフォルトのrotationPasswordUser_passwordから変化しました。

{
  "username": "rotationPasswordUser"
  "password": "rV8-a3kUB6UH8Xy^#YZ&Wk+dz(5Cl&zG",
  "dbClusterIdentifier": "dbClusterIdentifier",
  "dbname": "testDB",
  "engine": "postgres",
  "port": "5432",
  "host": "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com",
}

シークレットのローテーション設定変更

続いて、マルチユーザーローテーションでパスワードをローテーションするようにシークレットの設定変更を行います。

今回もAWS CDKでマルチユーザーローテーションをするように定義します。マルチユーザーローテーションをする際は、ローテーション制御用DBユーザーのシークレットをmasterSecretを指定する必要があります。

実際のコードは以下の通りです。

// Rotate DB Admin user secret
new secretsmanager.SecretRotation(this, "DbAdminSecretRotationMultiUser", {
  application:
    secretsmanager.SecretRotationApplication.POSTGRES_ROTATION_MULTI_USER,
  secret: dbAdminSecret,
  target: dbCluster,
  vpc: props.vpc,
  automaticallyAfter: Duration.days(3),
  excludeCharacters: EXCLUDE_CHARACTERS,
  masterSecret: dbRotationPasswordUserSecret,
  securityGroup: rotateSecretsLambdaFunctionSg,
  vpcSubnets: props.vpc.selectSubnets({
    subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
  }),
});

npx cdk deploy DatabaseStackで変更を更新します。

ここで、正しくパスワードのローテーションされたかどうかを確認するために、マルチユーザーローテーションをするLambda関数のCloudWatch Logsを確認します。

CloudWatch Logsを確認すると、以下のようにエラーが出力されてました。

START RequestId: 6ce7e854-dcb5-448d-8b7d-c081e22a3dec Version: $LATEST
[INFO]	2022-03-14T02:07:25.624Z	6ce7e854-dcb5-448d-8b7d-c081e22a3dec	Found credentials in environment variables.
[INFO]	2022-03-14T02:07:26.896Z	6ce7e854-dcb5-448d-8b7d-c081e22a3dec	createSecret: Successfully put secret for ARN arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-SfkpWm and version 947cb4d9-7885-400c-87e0-15483b395cfa.
END RequestId: 6ce7e854-dcb5-448d-8b7d-c081e22a3dec
REPORT RequestId: 6ce7e854-dcb5-448d-8b7d-c081e22a3dec	Duration: 1544.24 ms	Billed Duration: 1545 ms	Memory Size: 128 MB	Max Memory Used: 72 MB	Init Duration: 386.31 ms	

START RequestId: 095ccd92-6fcc-4d31-832f-237b25aec9e2 Version: $LATEST
[ERROR] KeyError: 'masterarn'
Traceback (most recent call last):
  File "/var/task/lambda_function.py", line 79, in lambda_handler
    set_secret(service_client, arn, token)
  File "/var/task/lambda_function.py", line 174, in set_secret
    master_arn = current_dict['masterarn']
END RequestId: 095ccd92-6fcc-4d31-832f-237b25aec9e2
REPORT RequestId: 095ccd92-6fcc-4d31-832f-237b25aec9e2	Duration: 739.85 ms	Billed Duration: 740 ms	Memory Size: 128 MB	Max Memory Used: 74 MB	

どうやらmasterarnというキーが存在しないためエラーが出力されているようです。

AWS公式ドキュメントでマルチユーザーローテーションを行う場合のシークレットの構造を確認すると、確かにmasterarnが必要でした。

{
  "engine": "postgres",
  "host": "<required: instance host name/resolvable DNS name>",
  "username": "<required: username>",
  "password": "<required: password>",
  "dbname": "<optional: database name. If not specified, defaults to 'postgres'>",
  "port": "<optional: TCP port number. If not specified, defaults to 5432>",
  "masterarn": "<required: the ARN of the elevated secret used to create 2nd user and change passwords>"
}

マネージメントコンソールでローテーションの設定を確認すると、「個別の認証情報を使用してこのシークレットをローテーション」が「いいえ」のままとなっていました。マルチユーザーローテーションをするLambda関数を使用するのにも関わらず、こちらの設定が「いいえ」になっているため、masterarnが設定されていなかったようです。

そのため、こちらを「はい」に変更して、使用するシークレットとしてローテーション制御用DBユーザーのシークレットを指定して、保存します。

ローテーションの設定

保存後にシークレットを確認すると、masterarnが追加されていることと、パスワードが更新されていることが確認できました。

{
  "dbClusterIdentifier": "prd-db-cluster",
  "password": "eO4g2E~oTh3.=}]Y!OdZS7l[g8BF3ikR",
  "dbname": "testDB",
  "engine": "postgres",
  "port": 5432,
  "host": "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com",
  "username": "postgresAdmin_clone",
  "masterarn": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/rotationPasswordUserLoginInfo-8PafI6"
}

そして皆さんお気づきでしょうか。「パスワードだけでなくDBユーザー名も変わっていることを

AWS公式ドキュメントを確認すると、ユーザーをクローンと書いてありますね。

この戦略を使用するには、もう 1 つのユーザーの作成と、両方のユーザーに対するパスワードの変更が許可された、管理者またはスーパーユーザーの認証情報を持つ別のシークレットが必要です。最初のローテーションでは、このユーザーをクローンして交代ユーザーを作成することで、両方のユーザーが同じ権限を持つようにします。

ローテーション戦略 - AWS Secrets Manager

それでは、以前のバージョンのシークレットと比較をしてみましょう。

まず、AWS CLIでこのシークレットのバージョン一覧を確認します。

$ aws secretsmanager list-secret-version-ids \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-SfkpWm",
    "Versions": [
        {
            "VersionId": "e029a87c-d278-4f8c-a937-32c59d2f066f",
            "VersionStages": [
                "AWSCURRENT",
                "AWSPENDING"
            ],
            "LastAccessedDate": 1647216000.0,
            "CreatedDate": 1647224746.39
        },
        {
            "VersionId": "ec1c69f0-768c-483a-8c4a-de9ebf01ac3f",
            "VersionStages": [
                "AWSPREVIOUS"
            ],
            "LastAccessedDate": 1647216000.0,
            "CreatedDate": 1647224381.223
        }
    ]
}

AWSCURRENTとなっているバージョンのシークレットから確認します。こちらのusernamepostgresAdmin_cloneですね。

$ aws secretsmanager get-secret-value \
>     --secret-id prd-db-cluster/AdminLoginInfo \
>     --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "VersionId": "e029a87c-d278-4f8c-a937-32c59d2f066f",
    "SecretString": "{\"dbClusterIdentifier\": \"prd-db-cluster\", \"password\": \"eO4g2E~oTh3.=}]Y!OdZS7l[g8BF3ikR\", \"dbname\": \"testDB\", \"engine\": \"postgres\", \"port\": 5432, \"host\": \"prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com\", \"username\": \"postgresAdmin_clone\", \"masterarn\": \"arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/rotationPasswordUserLoginInfo-8PafI6\"}",
    "VersionStages": [
        "AWSCURRENT",
        "AWSPENDING"
    ],
    "CreatedDate": 1647224746.39,
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-SfkpWm"
}

もう一つのバージョンのシークレットも確認します。こちらのusernamepostgresAdminですね。

$ aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --version-id ec1c69f0-768c-483a-8c4a-de9ebf01ac3f \
     --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "VersionId": "ec1c69f0-768c-483a-8c4a-de9ebf01ac3f",
    "SecretString": "{\n    \"dbClusterIdentifier\": \"prd-db-cluster\",\n    \"password\": \"+2gf?F0bk3estdeXri#cq6$Ux{8}~crC\",\n    \"dbname\": \"testDB\",\n    \"engine\": \"postgres\",\n    \"port\": 5432,\n    \"host\": \"prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com\",\n    \"username\": \"postgresAdmin\",\n    \"masterarn\": \"arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/rotationPasswordUserLoginInfo-8PafI6\"\n}",
    "VersionStages": [
        "AWSPREVIOUS"
    ],
    "CreatedDate": 1647224381.223,
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-SfkpWm"
}

この2つのバージョンのシークレットを使って、それぞれDBに接続できるか確認します。

$ get_secrets_value=$(aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --version-id ec1c69f0-768c-483a-8c4a-de9ebf01ac3f \
     --region us-east-1 \
     | jq -r .SecretString)

$ export PGHOST=$(echo "${get_secrets_value}" | jq -r .host)
$ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port)
$ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname)
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

$ psql
psql (13.6, server 13.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

testDB=>
testDB=>
testDB=> \conninfo
You are connected to database "testDB" as user "postgresAdmin" on host "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com" (address "10.10.0.84") at port "5432".
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
testDB=>
testDB=> \du
                                                                  List of roles
      Role name       |                         Attributes                         |                          Member of

----------------------+------------------------------------------------------------+----------------------------------------------------
---------
 postgresAdmin        | Create role, Create DB                                    +| {rds_superuser}
                      | Password valid until infinity                              |
 postgresAdmin_clone  |                                                            | {postgresAdmin}
 rds_ad               | Cannot login                                               | {}
 rds_iam              | Cannot login                                               | {}
 rds_password         | Cannot login                                               | {}
 rds_replication      | Cannot login                                               | {}
 rds_superuser        | Cannot login                                               | {pg_monitor,pg_signal_backend,rds_replication,rds_p
assword}
 rdsadmin             | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
                      | Password valid until infinity                              |
 rotationPasswordUser | Create role                                                | {}

testDB=>
testDB=> exit

$ get_secrets_value=$(aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --region us-east-1 \
     | jq -r .SecretString)

$ export PGHOST=$(echo "${get_secrets_value}" | jq -r .host)
$ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port)
$ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname)
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

$ psql
psql (13.6, server 13.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

testDB=>
testDB=> \conninfo
You are connected to database "testDB" as user "postgresAdmin_clone" on host "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com" (address "10.10.0.84") at port "5432".
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
testDB=>
testDB=> \du
                                                                  List of roles
      Role name       |                         Attributes                         |                          Member of

----------------------+------------------------------------------------------------+----------------------------------------------------
---------
 postgresAdmin        | Create role, Create DB                                    +| {rds_superuser}
                      | Password valid until infinity                              |
 postgresAdmin_clone  |                                                            | {postgresAdmin}
 rds_ad               | Cannot login                                               | {}
 rds_iam              | Cannot login                                               | {}
 rds_password         | Cannot login                                               | {}
 rds_replication      | Cannot login                                               | {}
 rds_superuser        | Cannot login                                               | {pg_monitor,pg_signal_backend,rds_replication,rds_p
assword}
 rdsadmin             | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
                      | Password valid until infinity                              |
 rotationPasswordUser | Create role                                                | {}

testDB=>
testDB=> exit

どちらのバージョンのシークレットでもアクセスできているようですね。また、postgresAdmin_cloneというユーザー(ロール)が追加されており、postgresAdminと同じ権限が付与されていることが確認できました。

それでは、もう一度ローテーションして、どのような値になるのかを確認します。

手動でローテーションをした後、バージョン一覧と各バージョンのシークレットを確認しました。

$ aws secretsmanager list-secret-version-ids \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-SfkpWm",
    "Versions": [
        {
            "VersionId": "e029a87c-d278-4f8c-a937-32c59d2f066f",
            "VersionStages": [
                "AWSPREVIOUS"
            ],
            "LastAccessedDate": 1647216000.0,
            "CreatedDate": 1647224746.39
        },
        {
            "VersionId": "49108bbf-8a5b-41fe-ba3d-21909976627e",
            "VersionStages": [
                "AWSCURRENT",
                "AWSPENDING"
            ],
            "LastAccessedDate": 1647216000.0,
            "CreatedDate": 1647228720.804
        }
    ]
}

$ aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --version-id e029a87c-d278-4f8c-a937-32c59d2f066f \
     --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "VersionId": "e029a87c-d278-4f8c-a937-32c59d2f066f",
    "SecretString": "{\"dbClusterIdentifier\": \"prd-db-cluster\", \"password\": \"eO4g2E~oTh3.=}]Y!OdZS7l[g8BF3ikR\", \"dbname\": \"testDB\", \"engine\": \"postgres\", \"port\": 5432, \"host\": \"prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com\", \"username\": \"postgresAdmin_clone\", \"masterarn\": \"arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/rotationPasswordUserLoginInfo-8PafI6\"}",
    "VersionStages": [
        "AWSPREVIOUS"
    ],
    "CreatedDate": 1647224746.39,
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-SfkpWm"
}

$ aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --version-id 49108bbf-8a5b-41fe-ba3d-21909976627e \
     --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "VersionId": "49108bbf-8a5b-41fe-ba3d-21909976627e",
    "SecretString": "{\"dbClusterIdentifier\": \"prd-db-cluster\", \"password\": \"EZ{4wMD4Xp=>t5>?*Z]l#[n2]!eeSzKQ\", \"dbname\": \"testDB\", \"engine\": \"postgres\", \"port\": 5432, \"host\": \"prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com\", \"username\": \"postgresAdmin\", \"masterarn\": \"arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/rotationPasswordUserLoginInfo-8PafI6\"}",
    "VersionStages": [
        "AWSCURRENT",
        "AWSPENDING"
    ],
    "CreatedDate": 1647228720.804,
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-SfkpWm"
}

ローテーション前はAWSCURRENTだったバージョンがAWSPREVIOUSに変化しています。また、usernamepostgresAdminとなっているバージョンがAWSCURRENTとなっています。

どうやらマルチユーザーローテーションの場合、postgresAdminpostgresAdmin_cloneのパスワードを交互に更新してラベルを張り替える動きをするようです。

それでは、最後に2つのバージョンのシークレットを使って、それぞれDBに接続できるか確認します。

$ get_secrets_value=$(aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --version-id e029a87c-d278-4f8c-a937-32c59d2f066f \
     --region us-east-1 \
     | jq -r .SecretString)

$ export PGHOST=$(echo "${get_secrets_value}" | jq -r .host)
$ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port)
$ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname)
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

$ psql
psql (13.6, server 13.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

testDB=> \conninfo
You are connected to database "testDB" as user "postgresAdmin_clone" on host "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com" (address "10.10.0.84") at port "5432".
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
testDB=>
testDB=> \du
                                                                  List of roles
      Role name       |                         Attributes                         |                          Member of

----------------------+------------------------------------------------------------+----------------------------------------------------
---------
 postgresAdmin        | Create role, Create DB                                    +| {rds_superuser}
                      | Password valid until infinity                              |
 postgresAdmin_clone  |                                                            | {postgresAdmin}
 rds_ad               | Cannot login                                               | {}
 rds_iam              | Cannot login                                               | {}
 rds_password         | Cannot login                                               | {}
 rds_replication      | Cannot login                                               | {}
 rds_superuser        | Cannot login                                               | {pg_monitor,pg_signal_backend,rds_replication,rds_p
assword}
 rdsadmin             | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
                      | Password valid until infinity                              |
 rotationPasswordUser | Create role                                                | {}

testDB=>
testDB=> exit

$ get_secrets_value=$(aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --region us-east-1 \
     | jq -r .SecretString)

$ export PGHOST=$(echo "${get_secrets_value}" | jq -r .host)
$ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port)
$ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname)
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

$ psql
psql (13.6, server 13.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

testDB=>
testDB=> \conninfo
You are connected to database "testDB" as user "postgresAdmin" on host "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com" (address "10.10.0.84") at port "5432".
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
testDB=>
testDB=> \du
                                                                  List of roles
      Role name       |                         Attributes                         |                          Member of

----------------------+------------------------------------------------------------+----------------------------------------------------
---------
 postgresAdmin        | Create role, Create DB                                    +| {rds_superuser}
                      | Password valid until infinity                              |
 postgresAdmin_clone  |                                                            | {postgresAdmin}
 rds_ad               | Cannot login                                               | {}
 rds_iam              | Cannot login                                               | {}
 rds_password         | Cannot login                                               | {}
 rds_replication      | Cannot login                                               | {}
 rds_superuser        | Cannot login                                               | {pg_monitor,pg_signal_backend,rds_replication,rds_p
assword}
 rdsadmin             | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
                      | Password valid until infinity                              |
 rotationPasswordUser | Create role                                                | {}

testDB=>

どちらのバージョンのシークレットでもDBに接続できました。

追加検証 : 自身のARNを masterarn としてローテーションした場合

自身のARNをmasterarnとしてローテーションした場合にどのような挙動をするのか気になったので、確認してみます。

一度Aurora DB Clusterを作り直して、綺麗な状態で検証を行います。

デフォルトのシークレットは以下のようになっています。

{
  "dbClusterIdentifier": "prd-db-cluster",
  "password": "G2K}e\\viYZenPK7)Tqw0UY0gE;Ro2>S(",
  "dbname": "testDB",
  "engine": "postgres",
  "port": 5432,
  "host": "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com",
  "username": "postgresAdmin"
}

個別の認証情報を使用してこのシークレットをローテーション」を「はい」に変更し、使用するシークレットとして自身を指定して保存します。

自身のシークレットを指定

保存後にシークレットを確認すると、masterarnが追加されていることと、パスワードが更新されていることが確認できました。

{
  "dbClusterIdentifier": "prd-db-cluster",
  "password": "7Y>o`u4&J4H46A,Jq1W{gBQi~{vc!i^Z",
  "dbname": "testDB",
  "engine": "postgres",
  "port": 5432,
  "host": "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com",
  "username": "postgresAdmin_clone",
  "masterarn": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73"
}

CloudWatch Logsを確認しましたが、正常にローテーションされていそうです。

START RequestId: c41bbd21-bc24-43d5-8095-d2c8abcf708d Version: $LATEST
[INFO]	2022-03-15T04:31:39.672Z	c41bbd21-bc24-43d5-8095-d2c8abcf708d	createSecret: Successfully put secret for ARN arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73 and version f60f37cd-d6e7-46a9-a46d-aac9bfe03579.
END RequestId: c41bbd21-bc24-43d5-8095-d2c8abcf708d
REPORT RequestId: c41bbd21-bc24-43d5-8095-d2c8abcf708d	Duration: 437.41 ms	Billed Duration: 438 ms	Memory Size: 128 MB	Max Memory Used: 75 MB	

START RequestId: 4b70a7ba-9f72-4aa1-962a-3e37e111e315 Version: $LATEST
[INFO]	2022-03-15T04:31:40.290Z	4b70a7ba-9f72-4aa1-962a-3e37e111e315	setSecret: Successfully created user postgresAdmin_clone in PostgreSQL DB for secret arn arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73.
END RequestId: 4b70a7ba-9f72-4aa1-962a-3e37e111e315
REPORT RequestId: 4b70a7ba-9f72-4aa1-962a-3e37e111e315	Duration: 568.21 ms	Billed Duration: 569 ms	Memory Size: 128 MB	Max Memory Used: 75 MB	

START RequestId: c86fc4f7-d238-4ac8-a232-aeeca7f45c42 Version: $LATEST
[INFO]	2022-03-15T04:31:40.638Z	c86fc4f7-d238-4ac8-a232-aeeca7f45c42	testSecret: Successfully signed into PostgreSQL DB with AWSPENDING secret in arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73.
END RequestId: c86fc4f7-d238-4ac8-a232-aeeca7f45c42
REPORT RequestId: c86fc4f7-d238-4ac8-a232-aeeca7f45c42	Duration: 340.56 ms	Billed Duration: 341 ms	Memory Size: 128 MB	Max Memory Used: 75 MB	

START RequestId: de3b63c5-c9dd-4fcf-9303-8a0a1ed21a44 Version: $LATEST
[INFO]	2022-03-15T04:31:40.947Z	de3b63c5-c9dd-4fcf-9303-8a0a1ed21a44	finishSecret: Successfully set AWSCURRENT stage to version f60f37cd-d6e7-46a9-a46d-aac9bfe03579 for secret arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73.
END RequestId: de3b63c5-c9dd-4fcf-9303-8a0a1ed21a44
REPORT RequestId: de3b63c5-c9dd-4fcf-9303-8a0a1ed21a44	Duration: 308.06 ms	Billed Duration: 309 ms	Memory Size: 128 MB	Max Memory Used: 75 MB	

また、このシークレットでDBに接続しようとしたところ、正しく接続されました。

$ get_secrets_value=$(aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --region us-east-1 \
     | jq -r .SecretString)

$ export PGHOST=$(echo "${get_secrets_value}" | jq -r .host)
$ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port)
$ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname)
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

psql (13.6, server 13.4)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

testDB=>
testDB=>
testDB=> \conninfo
You are connected to database "testDB" as user "postgresAdmin_clone" on host "prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com" (address "10.10.0.93") at port "5432".
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
testDB=>
testDB=> \du
                                                                 List of roles
      Role name      |                         Attributes                         |                          Member of
---------------------+------------------------------------------------------------+-------------------------------------------------------------
 postgresAdmin       | Create role, Create DB                                    +| {rds_superuser}
                     | Password valid until infinity                              |
 postgresAdmin_clone |                                                            | {postgresAdmin}
 rds_ad              | Cannot login                                               | {}
 rds_iam             | Cannot login                                               | {}
 rds_password        | Cannot login                                               | {}
 rds_replication     | Cannot login                                               | {}
 rds_superuser       | Cannot login                                               | {pg_monitor,pg_signal_backend,rds_replication,rds_password}
 rdsadmin            | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
                     | Password valid until infinity                              |

testDB=>

このままでは「ローテーション制御用DBユーザーのシークレットなんかいらないのでは?」となってしまいます。試しにもう一度シークレットをローテーションさせてみます。

今回はすぐにシークレットをローテーションさせるをクリックしても、シークレットは更新されませんでした。マルチユーザーローテーションをするLambda関数のCloudWatch Logsを確認すると、以下のようにエラーが出力されてました。

START RequestId: 5f27ca62-3117-45bd-9396-658bb4332873 Version: $LATEST
[INFO]	2022-03-15T04:49:49.025Z	5f27ca62-3117-45bd-9396-658bb4332873	Found credentials in environment variables.
[INFO]	2022-03-15T04:49:50.145Z	5f27ca62-3117-45bd-9396-658bb4332873	createSecret: Successfully put secret for ARN arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73 and version 13ae6b86-1962-41ed-832e-cf0e435be75d.
END RequestId: 5f27ca62-3117-45bd-9396-658bb4332873
REPORT RequestId: 5f27ca62-3117-45bd-9396-658bb4332873	Duration: 1387.63 ms	Billed Duration: 1388 ms	Memory Size: 128 MB	Max Memory Used: 72 MB	Init Duration: 360.00 ms	

START RequestId: 9af60864-97eb-421e-9457-71d5be16e218 Version: $LATEST
[ERROR] ProgrammingError: ERROR:  permission denied
Traceback (most recent call last):
  File "/var/task/lambda_function.py", line 79, in lambda_handler
    set_secret(service_client, arn, token)
  File "/var/task/lambda_function.py", line 197, in set_secret
    cur.execute(alter_role + " WITH PASSWORD %s", (pending_dict['password'],))
  File "/var/task/pgdb.py", line 1034, in execute
    return self.executemany(operation, [parameters])
  File "/var/task/pgdb.py", line 1058, in executemany
    rows = self._src.execute(sql)
END RequestId: 9af60864-97eb-421e-9457-71d5be16e218
REPORT RequestId: 9af60864-97eb-421e-9457-71d5be16e218	Duration: 641.90 ms	Billed Duration: 642 ms	Memory Size: 128 MB	Max Memory Used: 74 MB	

マルチユーザーローテーションをするLambda関数のソースコードはGitHubから確認できます。

エラーが出ているset_secret(service_client, arn, token)を確認します。こちらの関数にコメントに以下のような記載がありました。

"""Set the pending secret in the database

This method tries to login to the database with the AWSPENDING secret and returns on success. If that fails, it tries to login with the master credentials from the masterarn in the current secret. If this succeeds, it adds all grants for AWSCURRENT user to the AWSPENDING user, creating the user and/or setting the password in the process. Else, it throws a ValueError.

Args: service_client (client): The secrets manager service client arn (string): The secret ARN or other identifier token (string): The ClientRequestToken associated with the secret version

Raises: ResourceNotFoundException: If the secret with the specified arn and stage does not exist ValueError: If the secret is not valid JSON or master credentials could not be used to login to DB KeyError: If the secret json does not contain the expected keys """

コメントとエラー文面から、どうやらDB接続後にALTER USERでパスワードを変更しようとしたときに失敗していますね。原因はpostgresAdmin_cloneが継承元であるpostgresAdminのパスワードを変更しようとしたためです。

そのため、「マルチユーザーローテーションをする場合、別途シークレットのローテーションを管理するためのシークレットが必要になる」ということですね。

おまけで、ローテーションに失敗した時のシークレットのバージョンを確認します。新しいバージョンが作成されてはいますが、AWSCURRENTにはならずに、AWSPENDINGのままとなっています。

$ aws secretsmanager list-secret-version-ids \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73",
    "Versions": [
        {
            "VersionId": "13ae6b86-1962-41ed-832e-cf0e435be75d",
            "VersionStages": [
                "AWSPENDING"
            ],
            "LastAccessedDate": 1647302400.0,
            "CreatedDate": 1647319790.129
        },
        {
            "VersionId": "f60f37cd-d6e7-46a9-a46d-aac9bfe03579",
            "VersionStages": [
                "AWSCURRENT"
            ],
            "LastAccessedDate": 1647302400.0,
            "CreatedDate": 1647318699.657
        },
        {
            "VersionId": "3d1d1943-f7c1-4a70-a434-953e666165a7",
            "VersionStages": [
                "AWSPREVIOUS"
            ],
            "LastAccessedDate": 1647302400.0,
            "CreatedDate": 1647318698.736
        }
    ]
}

$ aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --version-id 13ae6b86-1962-41ed-832e-cf0e435be75d \
     --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "VersionId": "13ae6b86-1962-41ed-832e-cf0e435be75d",
    "SecretString": "{\"dbClusterIdentifier\": \"prd-db-cluster\", \"password\": \"OB)TVp=B~LtxyPS10xz+(XqKdD;<5FHL\", \"dbname\": \"testDB\", \"engine\": \"postgres\", \"port\": 5432, \"host\": \"prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com\", \"username\": \"postgresAdmin\", \"masterarn\": \"arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73\"}",
    "VersionStages": [
        "AWSPENDING"
    ],
    "CreatedDate": 1647319790.129,
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73"
}

$ aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --version-id f60f37cd-d6e7-46a9-a46d-aac9bfe03579 \
     --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "VersionId": "f60f37cd-d6e7-46a9-a46d-aac9bfe03579",
    "SecretString": "{\"dbClusterIdentifier\": \"prd-db-cluster\", \"password\": \"7Y>o`u4&J4H46A,Jq1W{gBQi~{vc!i^Z\", \"dbname\": \"testDB\", \"engine\": \"postgres\", \"port\": 5432, \"host\": \"prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com\", \"username\": \"postgresAdmin_clone\", \"masterarn\": \"arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": 1647318699.657,
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73"
}

$ aws secretsmanager get-secret-value \
     --secret-id prd-db-cluster/AdminLoginInfo \
     --version-id 3d1d1943-f7c1-4a70-a434-953e666165a7 \
     --region us-east-1
{
    "Name": "prd-db-cluster/AdminLoginInfo",
    "VersionId": "3d1d1943-f7c1-4a70-a434-953e666165a7",
    "SecretString": "{\"dbClusterIdentifier\":\"prd-db-cluster\",\"password\":\"G2K}e\\\\viYZenPK7)Tqw0UY0gE;Ro2>S(\",\"dbname\":\"testDB\",\"engine\":\"postgres\",\"port\":5432,\"host\":\"prd-db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com\",\"username\":\"postgresAdmin\",\"masterarn\":\"arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73\"}",
    "VersionStages": [
        "AWSPREVIOUS"
    ],
    "CreatedDate": 1647318698.736,
    "ARN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:prd-db-cluster/AdminLoginInfo-mBSK73"
}
$

マルチユーザーローテーション便利

AWS Secrets Managerのマルチユーザーローテーションを試してみました。

一つ前のバージョンのシークレットでもDBに接続できるので、シークレットをアプリケーション側でキャッシュしたとしても2回ローテーションされない限り、接続できるというのが嬉しいですね。

シングルユーザーローテーションの場合は、シークレットをアプリケーション側でキャッシュしていると、シークレット情報がローテーションされたときにアプリケーションからDBにアクセスできなくなります。そのため、「DBに接続できない場合、DBに接続するシークレットのキャッシュをクリアする」といった一工夫が必要になります。

マルチユーザーローテーションの場合は、キャッシュの保持期間をローテーション間隔よりも短くするだけなので、アプリケーション側の改修が少なくなります。

ただし、追加でローテーション制御用のDBユーザーおよび、そのユーザーのシークレットの管理が必要になるので注意が必要です。

また、今回使った検証で使ったAWS CDKのコードのリポジトリは以下になります。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.